package com.mike;

import jargs.gnu.CmdLineParser;
import jargs.gnu.CmdLineParser.IllegalOptionValueException;
import jargs.gnu.CmdLineParser.Option;
import jargs.gnu.CmdLineParser.UnknownOptionException;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import junit.framework.JUnit4TestAdapter;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

import com.mike.model.Assignment;
import com.mike.model.FinalProject;
import com.mike.model.ModelValidationResult;
import com.mike.test.TestCSSValidator;
import com.mike.test.TestHTMLValidator;
import com.mike.validators.IsCSSValidator;
import com.mike.validators.IsHTMLValidator;
import com.mike.validators.W3CValidator;

import edu.uci.ics.crawler4j.crawler.CrawlConfig;
import edu.uci.ics.crawler4j.crawler.CrawlController;
import edu.uci.ics.crawler4j.fetcher.PageFetcher;
import edu.uci.ics.crawler4j.robotstxt.RobotstxtConfig;
import edu.uci.ics.crawler4j.robotstxt.RobotstxtServer;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;


public class Helper {

	private enum Mode { Test, Help, ValidateFiles, ValidateUrls }
	
	private static final String s_outputFileName	= "output";
	private String m_studentFolder;
	private Integer	m_chapterNumber;
	private String m_urlsFile;
	
	private CmdLineParser parser = new CmdLineParser();
	private Option test = parser.addBooleanOption('t', "test");
	private Option outputDir = parser.addStringOption('o', "outputFileLocation");
	private Option filesFolder = parser.addStringOption('f', "filesFolder");
	private Option chapterNumber = parser.addIntegerOption('c', "chapterNumber");
	private Option checkWebsites = parser.addBooleanOption('w', "checkWebsites");
	private Option websiteNamesFile = parser.addStringOption('n', "websiteNamesFile");
	private Option help = parser.addBooleanOption('h', "help");
	private BufferedWriter m_writer;
	Mode mode;
	
	public Helper(){}
	
	public Optional<Helper> init(String[] args) {
		try {
			parser.parse(args);
		} catch (IllegalOptionValueException | UnknownOptionException e) {
			e.printStackTrace();
			return Optional.empty();
		}
		if(parser.getOptionValue(help) != null){
			mode = Mode.Help;
			return Optional.of(this);
		}
		if(parser.getOptionValue(test) != null){
			mode = Mode.Test;
			return Optional.of(this);
		}
		boolean checkingWebsites = (Boolean)parser.getOptionValue(checkWebsites, false);
		m_studentFolder = (String)parser.getOptionValue(filesFolder, "");
		m_chapterNumber = (Integer)parser.getOptionValue(chapterNumber, 0);
		m_urlsFile = (String)parser.getOptionValue(websiteNamesFile, "");
		if(!checkingWebsites){
			if(m_studentFolder == null || m_studentFolder.isEmpty()){
				System.err.println("Please specify a folder containing all of the student files.");
				return Optional.empty();
			}
			if(m_chapterNumber == null || m_chapterNumber == 0){
				System.err.println("When specifying student files, please specify the chapter number");
				return Optional.empty();
			}
			mode = Mode.ValidateFiles;
		} else {
			if(m_urlsFile == null || m_urlsFile.isEmpty()){
				System.err.println("When specifying to crawl web pages, please specify the file containing the listing of urls.");
				return Optional.empty();
			}
			mode = Mode.ValidateUrls;
		}
		
		String outDir = (String)parser.getOptionValue(outputDir, System.getProperty("user.dir"));
		File outputDirFile = new File(outDir);
		if(!outputDirFile.isDirectory())
			return Optional.empty();
		File[] files = outputDirFile.listFiles((file, filename) -> filename.contains(s_outputFileName));
		String outputName = s_outputFileName;
		if(files.length > 0)
			outputName += String.format("(%d)", files.length);
		outputName += ".log";
		File outputFile = new File(outputDirFile.getAbsolutePath() + File.separator + outputName);
		
		try {
			m_writer = new BufferedWriter(new FileWriter(outputFile));
		} catch (IOException e) {
			return Optional.empty();
		}
		
		return Optional.of(this);
	}
	
	private void write(String line) throws IOException{
		if(m_writer == null){
			
		}
		m_writer.append(line);
		m_writer.flush();
	}
	
	private void writeln(String line) {
		try {
			write(line);
			m_writer.newLine();
			m_writer.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	private void validateDropboxFiles(final IsHTMLValidator htmlValidator, final IsCSSValidator cssValidator) {
		Configuration cfg = new Configuration();
		cfg.setClassForTemplateLoading(Helper.class, "");
		cfg.setObjectWrapper(new DefaultObjectWrapper());
		cfg.setDefaultEncoding("UTF-8");
		cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
		
		final String feedbackFileName = "feedback.html";
		
		File studentFilesDir = new File(m_studentFolder);
		if(!studentFilesDir.isDirectory())
		{
			writeln(studentFilesDir.getAbsolutePath() + " is not a directory.");
			return;
		}
		
		Stream.of(studentFilesDir.listFiles(f -> f.isDirectory()))
		.filter(studentDir -> studentDir.listFiles( file -> file.getName().equals(feedbackFileName)).length == 0 )
		.forEach(studentDir -> {
			System.out.println("checking student files for "+studentDir.getName());
			
			Assignment assignment = new Assignment();
			assignment.chapterNum = m_chapterNumber;
			assignment.studentName = studentDir.getName();

			assignment.results = Stream.of(studentDir.listFiles())
				.filter(file -> file.getName().endsWith("html") || file.getName().endsWith("htm") || file.getName().endsWith("css"))
				.map(file ->  {
					try {
						return file.getName().endsWith("css") ? cssValidator.validateCSS(file) : htmlValidator.validateHTML(file);
					} catch (Exception e){
						System.err.println("Problem encountered when validating file "+file.getName()+" : "+e.getMessage());
						return null;
					}
				})
				.filter(r -> r != null)
				.map(validationResult -> new ModelValidationResult(validationResult))
				.collect(Collectors.toCollection(ArrayList::new));
			
			long numOK = assignment.results.stream().filter(r -> !r.hasError()).count();
			assignment.validationPoints = Double.valueOf(scale(numOK, 0, assignment.results.size(), 0, 6)).intValue();;
			try {
				Template template = cfg.getTemplate("feedbackTemplate.ftl");
				Writer out = new FileWriter(new File(studentDir.getAbsolutePath()+File.separator+feedbackFileName));
				template.process(AutoJSON.toJson(assignment), out);  
			} catch (Exception e) {
				e.printStackTrace();
			}  
			
			Helper.this.writeln("");
		});
	}
	
	public static double scale(final double valueIn, final double baseMin, final double baseMax, final double limitMin, final double limitMax) {
        return ((limitMax - limitMin) * (valueIn - baseMin) / (baseMax - baseMin)) + limitMin;
    }
	
	@SuppressWarnings("unchecked")
	private void validateUrls(final IsHTMLValidator htmlValidator, final IsCSSValidator cssValidator) throws Exception{
		Configuration cfg = new Configuration();
		cfg.setClassForTemplateLoading(Helper.class, "");
		cfg.setObjectWrapper(new DefaultObjectWrapper());
		cfg.setDefaultEncoding("UTF-8");
		cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
		
		CrawlConfig crawlConfig = new CrawlConfig();
        crawlConfig.setCrawlStorageFolder(System.getProperty("user.dir"));
        
        PageFetcher pageFetcher = new PageFetcher(crawlConfig);
        RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
        RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher);
        CrawlController crawlController = new CrawlController(crawlConfig, pageFetcher, robotstxtServer);
        
       	ReadFile.readLines(m_urlsFile, (line) -> crawlController.addSeed(line));
       	
       	final Pattern userPattern = Pattern.compile("org.coloradomesa.edu/~([a-zA-Z0-9]+)/");
        
        crawlController.start(PageCrawler.class, 4);
        List<CrawlResult> urls = new ArrayList<CrawlResult>();
		crawlController.getCrawlersLocalData()
		.stream()
		.map(obj -> (Set<CrawlResult>)obj)
		.forEach(urlSet -> urls.addAll(urlSet));
		
		Map<String, List<CrawlResult>> user2Urls = urls.stream()
		.collect(Collectors.groupingBy(url -> {
			Matcher m = userPattern.matcher(url.getUrl());
			if(!m.find())
				return "ungrouped";
			String student = m.group(1);
			return student;
		}));
		
		user2Urls.keySet().stream().forEach(student -> {
			
			List<CrawlResult> userUrls = user2Urls.get(student);
			
			FinalProject finalProject = new FinalProject();
			finalProject.studentName = student;
			finalProject.numCSSDocuments = (int)userUrls.stream().filter(CrawlResult::isCSS).count();
			finalProject.numHTMLPages = (int)userUrls.stream().filter(CrawlResult::isHTML).count();
			finalProject.numInlineStyles = userUrls.stream().filter(CrawlResult::isHTML).mapToInt(CrawlResult::getNumInlineStyles).sum();
			finalProject.numEmbeddedStyles = userUrls.stream().filter(CrawlResult::isHTML).mapToInt(CrawlResult::getNumEmbeddedStyles).sum();
			
			finalProject.foundFooter = userUrls.stream().filter(CrawlResult::isHTML).filter(CrawlResult::foundFooter).count() > 0;
			finalProject.foundHeader = userUrls.stream().filter(CrawlResult::isHTML).filter(CrawlResult::foundHeader).count() > 0;
			finalProject.foundImg = userUrls.stream().filter(CrawlResult::isHTML).filter(CrawlResult::foundImg).count() > 0;
			finalProject.foundTable = userUrls.stream().filter(CrawlResult::isHTML).filter(CrawlResult::foundTable).count() > 0;
			finalProject.foundNav = userUrls.stream().filter(CrawlResult::isHTML).filter(CrawlResult::foundNav).count() > 0;
			
			List<ModelValidationResult> cssResults = userUrls
														.stream()
														.filter(CrawlResult::isCSS)
														.map(CrawlResult::getUrl)
														.map(urlStr -> {
															try { return new URL(urlStr); }
															catch (Exception e){ return null;  }
														})
														.filter(url -> url != null)
														.map(url -> {
															try {
																return cssValidator.validateCSS(url);
															} catch (Exception e){
																System.err.println("Problem encountered when validating url "+url.toString()+" : "+e.getMessage());
																return null;
															}
														})
														.filter(r -> r != null)
														.map(r -> new ModelValidationResult(r))
														.collect(Collectors.toList());
		
			List<ModelValidationResult> htmlResults = userUrls
													.stream()
													.filter(CrawlResult::isHTML)
													.map(CrawlResult::getUrl)
													.map(urlStr -> {
														try { return new URL(urlStr); }
														catch (Exception e){ return null; }
													})
													.filter(url -> url != null)
													.map(url -> {
															try {
																return htmlValidator.validateHTML(url);
															} catch (Exception e){
																System.err.println("Problem encountered when validating url "+url.toString()+" : "+e.getMessage());
																return null;
															}
														})
														.filter(r -> r != null)
													.map(r -> new ModelValidationResult(r))
													.collect(Collectors.toList());
			
			int numCSSValid = (int)cssResults.stream().filter(ModelValidationResult::hasNoErrors).count();
			int numHTMLValid = (int)htmlResults.stream().filter(ModelValidationResult::hasNoErrors).count();
			
			int numTags = userUrls.stream()
							.filter(CrawlResult::isHTML)
							.collect(Collectors.summingInt(CrawlResult::getNumTags));
			int numHTML5Tags = userUrls.stream()
								.filter(CrawlResult::isHTML)
								.collect(Collectors.summingInt(CrawlResult::getNumHTML5Tags));
			
			finalProject.percentHTML5 = numTags == 0 ? 0 : (double)numHTML5Tags/(double)numTags;
			finalProject.percentCSSValid = finalProject.numCSSDocuments == 0 ? 0 : (double)numCSSValid / (double)finalProject.numCSSDocuments;
			finalProject.percentHTMLValid = finalProject.numHTMLPages == 0 ? 0 : (double)numHTMLValid / (double)finalProject.numHTMLPages;
			
			finalProject.results = new ArrayList<ModelValidationResult>();
			finalProject.results.addAll(cssResults);
			finalProject.results.addAll(htmlResults);
			finalProject.print(new PrintWriter(m_writer));
		});
		
	}
	
	private void runTests(IsHTMLValidator htmlValidator, IsCSSValidator cssValidator){
		TestSuite testSuite = new TestSuite("Validator Test");
		testSuite.addTest(new JUnit4TestAdapter(TestCSSValidator.class));
		testSuite.addTest(new JUnit4TestAdapter(TestHTMLValidator.class));
		TestResult result = TestRunner.run(testSuite);
		System.out.println(result.toString());
		
	}
	
	private void printHelp(){
		BufferedReader in = new BufferedReader(new InputStreamReader(Helper.class.getResourceAsStream("help.txt")));
		try {
			String line = null;
			while((line = in.readLine()) != null) {
			  System.out.println(line);
			}
		} catch (Exception e){}
		finally {
			if(in != null)
			{ try { in.close(); } catch (Exception e2){}}
		}
	}
	
	public void doWork(IsHTMLValidator htmlValidator, IsCSSValidator cssValidator) {
		switch(mode){
		case Test:
			runTests(htmlValidator, cssValidator);
			break;
		case ValidateFiles:
			try {
				validateDropboxFiles(htmlValidator, cssValidator);
			} catch (Exception e){}
			break;
		case ValidateUrls:
			try {
				validateUrls(htmlValidator, cssValidator);
			} catch (Exception e){}
			break;
		case Help:
		default:
			printHelp();
			break;
		
		}
	}
	
	public static void main(String[] args) {
		Helper helper = new Helper();
		helper.init(args)
		.ifPresent( h -> h.doWork(new W3CValidator(), new W3CValidator()) );
	}

}
